/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package commons

import org.latestbit.smartobjects.smartidlc.*
import org.latestbit.smartobjects.smartidlc.model.*

/**
 * References linker for IDL model
 * @author abdulla-a
 */ 
public class IDLReferenceLinker implements IIDLReferenceLinker {
	def targetNamespace; 
	def loadedNamespaces;
	def IMessageReporter reporter;
	
	def void start(
			IDLNamespace targetNamespace, 
			Map<String, IDLNamespace> loadedNamespaces,
			CompilerOptions options,
			IMessageReporter reporter) throws IDLLinkerException {
		this.reporter = reporter;
		loadedNamespaces.put ( targetNamespace.name, targetNamespace );
		this.targetNamespace = targetNamespace;
		this.loadedNamespaces = loadedNamespaces;
		this.loadedNamespaces.each {
			processNamespace ( it.value );
		}
	}
	
	def validateUniqueNames ( container, message=null ) {
		def unique = [] as Set;
		def dups = [] as Set;
		container.each {
			unique.add(it.name) || dups.add(it.name);
		}
		if(!dups.empty) {
			throw new IDLLinkerException("There are the following ID duplicates found: "
					+dups + (message ? " "+message:"") );
		}
	}
	
	def lookupByName ( name, container ) {
		def result = null;
		container.each {
			if ( it.name.equals(name) ) {
				result = it;
			}
		}
		return result;
	}
	
	def lookupObjInNamespace ( name, namespace, scopes = [ 'typedefs', 'structures', 'exceptions', 'choices', 'enums', 'interfaces' ] ) {
		def obj = null;
		if ( obj == null && scopes.contains('typedefs') )
			obj = lookupByName ( name, namespace.typedefs );
		if ( obj == null && scopes.contains('structures') )
			obj = lookupByName ( name, namespace.structures );
		if ( obj == null && scopes.contains('exceptions') )
			obj = lookupByName ( name, namespace.exceptions );		
		if ( obj == null && scopes.contains('choices') )
			obj = lookupByName ( name, namespace.choices );
		if ( obj == null && scopes.contains('enums') )
			obj = lookupByName ( name, namespace.enums );
		if ( obj == null && scopes.contains('interfaces') )		
			obj = lookupByName ( name, namespace.interfaces );
		
		return obj;
	}
	
	def isImported (String name) {
		return name.matches(".+\\..+");
	}
	
	def toRelativeName ( String name ) {
		def parts = name.split("\\.");		
		if (parts.length <=1) {
			return name;
		}
		else {
			return parts[parts.length-1];
		}
	}
	
	def defaultScopes = [ 'typedefs', 'structures', 'exceptions', 'choices', 'enums', 'interfaces' ] as Set;
	
	def lookupRefName ( name, namespace, scopes = defaultScopes, overTypeDefs = false ) {
		def result = null;
		if ( !isImported(name) ) {
			// Trying to search in the current namespace
			result = lookupObjInNamespace ( name, namespace, scopes );
			if ( !result ) {
				for ( it in namespace.importedNamespaces ) {
					result = lookupObjInNamespace ( name, it, scopes );
					if ( result )
						break;
				}
			}
		}
		else {			
			def relativeName = toRelativeName ( name );
			for( it in loadedNamespaces ) {				
				
				result = lookupObjInNamespace ( relativeName, it.value, scopes );
				if ( result ) {
					if ( result.fullName.equals(name) )
						break;
					else
						result = null;
				}
			}
		}
		if ( overTypeDefs && result!=null && result instanceof IDLTypeDef ) {
			result = lookupRefName( result.refDataType.name, namespace, scopes, overTypeDefs );
		}
		return result;		
	}
	
	def lookupRefDataType ( dataType, namespace, scopes = defaultScopes ) {
		def result = null;
		if ( !dataType.isImported() ) {
			// Trying to search in the current namespace
			result = lookupObjInNamespace ( dataType.name, namespace, scopes );
			if ( !result ) {
				for ( it in namespace.importedNamespaces ) {
					result = lookupObjInNamespace ( dataType.name, it, scopes );
					if ( result ) {
						break;
					}
				}
			}
		}
		else {
			for ( it in loadedNamespaces ) {
				result = lookupObjInNamespace ( dataType.relativeName, it.value, scopes );
				if ( result ) { 
					if ( result.fullName.equals(dataType.name) )
						break;
					else
						result = null;
				}
			}				
		}
		
		return result;
	}
	
	def resolveDataType ( dataType, namespace, scopes = defaultScopes ) {
		def result = null;
		if ( dataType.typeId == IDLDataType.DataTypeId.QUALIFIED_NAME ) {
			result = lookupRefDataType ( dataType, namespace, scopes );
			if ( result ) {
				dataType.userObject = result;
			}
			else {
				throw new IDLLinkerException(
					"Unable to found datatype identifier '$dataType.name'");
			}		
		}
		else
		if ( dataType.typeId == IDLDataType.DataTypeId.COMPLEX ) {
			if ( dataType.complexType.complexItemDataType.typeId == IDLDataType.DataTypeId.QUALIFIED_NAME )
				dataType.complexType.complexItemDataType.userObject = resolveDataType ( dataType.complexType.complexItemDataType, namespace, scopes );
			if ( dataType.complexType.complexItemDataType.typeId == IDLDataType.DataTypeId.COMPLEX )
				resolveDataType ( dataType.complexType.complexItemDataType, namespace, scopes );
		}
		return result;
	}	
	
	def processNamespace ( namespace ) {
		linkImportedNamespaces ( namespace );
		
		// Check unique identifiers
		validateUniqueNamespaceIDs ( namespace );
		
		linkTypeDefs ( namespace );
		linkEnums ( namespace );
		linkStructures ( namespace );
		linkExceptions ( namespace );
		linkChoices ( namespace );
		linkInterfaces ( namespace );
	}
	
	def validateUniqueNamespaceIDs ( namespace ) {
		def allIdentifiers = [] as List;
		allIdentifiers.addAll ( namespace.typedefs );
		allIdentifiers.addAll ( namespace.enums );
		allIdentifiers.addAll ( namespace.structures );
		allIdentifiers.addAll ( namespace.choices );		
		allIdentifiers.addAll ( namespace.interfaces );
		validateUniqueNames ( allIdentifiers );		
	}
	
	def linkImportedNamespaces( sourceNamespace ) {
		sourceNamespace.importedNamespacesLinks.each {
			// Ignore self importing
			if ( it != sourceNamespace.name ) {
				def namespace = loadedNamespaces.get (it);
				if ( ! namespace ) {
					throw new IDLLinkerException("Unable to found an imported namespace: "+it);
				}
				else {
					// Avoid stack overflows for cycle importing
					if ( ! sourceNamespace.importedNamespaces.contains (namespace) ) {
						sourceNamespace.importedNamespaces.add ( namespace );
						linkImportedNamespaces ( namespace );
					}
				}
			}
		}
		sourceNamespace.referencesLinked = true;
	}
	

	def linkTypeDefs ( namespace ) {
		namespace.typedefs.each {
			linkTypeDef ( it, namespace );
		}
	}
	
	
	def linkTypeDef ( typeDef, namespace ) {
		if ( ! typeDef.referencesLinked ) {
			resolveDataType ( typeDef.refDataType, namespace );
			if(typeDef.refDataType.typeId.equals(IDLDataType.DataTypeId.QUALIFIED_NAME) 
					&& typeDef.refDataType.userObject instanceof IDLException)
				throw new IDLLinkerException("Typedef '${typeDef.name}' cannot be applied to exception");
			if(typeDef.refDataType.typeId.equals(IDLDataType.DataTypeId.QUALIFIED_NAME) 
					&& typeDef.refDataType.userObject instanceof IDLInterface)
				throw new IDLLinkerException("Typedef '${typeDef.name}' cannot be applied to interface");
			
			typeDef.referencesLinked = true;
		}
	}
	
	def linkEnums ( namespace ) {
		namespace.enums.each {
			if ( ! it.referencesLinked ) {			
				validateEnum ( it );
				it.referencesLinked = true;
			}
		}
	}
	
	def validateEnum ( srcEnum ) {
		def unique = [] as Set;
		def dups = [] as Set;
		def hasValue = -1;
		
		srcEnum.items.each {
			unique.add(it.first) || dups.add(it.first);
			if ( hasValue == -1 ) {
				hasValue = it.second;
			}
			else {
				if ( (hasValue && !it.second) || (!hasValue && it.second))
					throw new IDLLinkerException(
							"Enumeration '$srcEnum.name' doesn't define user value assigments for all enum items.");					
			}
		}
		
		if(!dups.empty) {
			throw new IDLLinkerException("There are the following ID duplicates found for enum '$srcEnum.name': "+dups);
		}		
	}
	
	def linkStructures ( namespace ) {
		namespace.structures.each {
			linkStructure ( it, namespace );
		}		
	}
	
	def getAllStructureItems ( structure, allItems, inheritancePath, namespace ) {
		def pathItem = inheritancePath.get(structure);
		
		if( pathItem == null ) {
			inheritancePath.put ( structure, true );
			if ( structure instanceof IDLTypeDef ) {
				def typeDef = structure;
				if ( ! typeDef.referencesLinked ) {
					linkTypeDef ( typeDef, typeDef.namespace );
				}

				if ( !typeDef.refDataType.typeId.equals(IDLDataType.DataTypeId.QUALIFIED_NAME) )
					throw new IDLLinkerException("Primitive type: ${typeDef.name} cannot define structure items!");
				if ( !typeDef.refDataType.userObject instanceof IDLTypeDef || !typeDef.refDataType.userObject instanceof IDLInterface )
					throw new IDLLinkerException("${typeDef.name} is not compatible data type for structures!");
				
				getAllStructureItems ( typeDef.refDataType.userObject, allItems, inheritancePath, typeDef.namespace );
			}
			else {			
				allItems.addAll ( structure.items );
				structure.parents.each {
					if(!it.referencesLinked)
						linkStructure ( it, it.namespace );				
					getAllStructureItems ( it, allItems, inheritancePath, it.namespace );
				};
			}
			inheritancePath[structure] = false;
		}
		else {
			if(pathItem.value)
				throw new IDLLinkerException("Found cyclic inheritance path for ${structure.name}!");
		}
	}
	
	def linkStructure ( structure, namespace ) {
		if ( structure.referencesLinked ) {
			return;
		}
		structure.items.each {
			resolveDataType ( it.type, namespace );
		}
		
		structure.parentsLinks.each {
			def parent = lookupRefName ( it, namespace, ["structures", "typedefs"] );
			if (!parent)
				throw new IDLLinkerException("Unable to find parent structure '$it' for '$structure.name'!");
			if (parent.is(structure))
				throw new IDLLinkerException("Structure '$structure.name' cannot be extend itself!");
			
			structure.parents.add ( parent );			
		}
		
		structure.referencesLinked = true;
		
		// Resolve all items for full inheritance path
		def allItems = [] as List;
		def inheritancePath = [:] as Map;
		getAllStructureItems ( structure, allItems, inheritancePath, namespace );
		
		validateUniqueNames ( allItems, "for '$structure.name'" );
		structure.setAllItems (allItems);
	}
	
	def linkExceptions ( namespace ) {
		namespace.exceptions.each {
			linkException ( it, namespace );
		}		
	}
	
	def getAllExceptionItems ( exceptionType, allItems, inheritancePath, namespace ) {
		def pathItem = inheritancePath.get(exceptionType);
		
		if( pathItem == null ) {
			inheritancePath.put ( exceptionType, true );
			allItems.addAll ( exceptionType.items );
			if(!exceptionType.parent.referencesLinked) {
				linkException ( exceptionType.parent, exceptionType.parent.namespace );				
				getAllExceptionItems ( exceptionType.parent, allItems, inheritancePath, exceptionType.parent.namespace );
			};
			inheritancePath[exceptionType] = false;
		}
		else {
			if(pathItem.value)
				throw new IDLLinkerException("Found cyclic inheritance path for ${exceptionType.name}!");
		}
	}
	
	def linkException ( exceptionType, namespace ) {
		if ( exceptionType.referencesLinked ) {
			return;
		}
		
		exceptionType.items.each {
			resolveDataType ( it.type, namespace );
		}
				
		def parent = null;
		if(exceptionType.parentLink!=null) {
			parent = lookupRefName ( exceptionType.parentLink, namespace, ["exceptions"] );
			if (!parent)
				throw new IDLLinkerException("Unable to find parent exception '$exceptionType.parentLink' for '$exceptionType.name'!");
			if (parent.is(exceptionType))
				throw new IDLLinkerException("Exception '$exceptionType.name' cannot be extend itself!");
		}
			
		exceptionType.parent = parent;			
		
		exceptionType.referencesLinked = true;
		
		// Resolve all items for full inheritance path
		def allItems = [] as List;
		if(exceptionType.parentLink!=null) {
			def inheritancePath = [:] as Map;
			getAllExceptionItems ( exceptionType, allItems, inheritancePath, namespace );
		}
		else
			allItems = exceptionType.items;
		
		validateUniqueNames ( allItems, "for '$exceptionType.name'" );
		exceptionType.setAllItems (allItems);
	}
	
	def getAllChoiceItems ( choice, allItems, inheritancePath, namespace ) {
		def pathItem = inheritancePath.get(choice);
		
		if( pathItem == null ) {
			inheritancePath.put ( choice, true );
			
			if ( choice instanceof IDLTypeDef ) {
				def typeDef = choice;
				if ( ! typeDef.referencesLinked ) {
					linkTypeDef ( typeDef, typeDef.namespace );
				}

				if ( !typeDef.refDataType.typeId.equals(IDLDataType.DataTypeId.QUALIFIED_NAME) )
					throw new IDLLinkerException("Primitive type: ${typeDef.name} cannot define choice items!");
				if ( !typeDef.refDataType.userObject instanceof IDLTypeDef || !typeDef.refDataType.userObject instanceof IDLInterface )
					throw new IDLLinkerException("${typeDef.name} is not compatible data type for choices!");
				
				getAllChoiceItems ( typeDef.refDataType.userObject, allItems, inheritancePath, typeDef.namespace );
			}
			else {			
				allItems.addAll ( choice.items );
				choice.parents.each {
					if(!it.referencesLinked)
						linkChoice ( it, it.namespace );				
					getAllChoiceItems ( it, allItems, inheritancePath, it.namespace );
				};
			}
			
			inheritancePath[choice] = false;
		}
		else {
			if(pathItem.value)
				throw new IDLLinkerException("Found cyclic inheritance path for ${choice.name}!");
		}
	}
	
	def linkChoices ( namespace ) {		
		namespace.choices.each {
			linkChoice ( it, namespace );
		}		
	}
	
	def linkChoice ( choice, namespace ) {
		if ( choice.referencesLinked ) {
			return;
		}		
		choice.items.each {
			resolveDataType ( it.type, namespace );
		}		
		
		choice.parentsLinks.each {
			def parent = lookupRefName ( it, namespace, ["choices", "typedefs"] );
			if (!parent)
				throw new IDLLinkerException("Unable to find parent choice '$it' for '$choice.name'!");
			if (parent.is(choice))
				throw new IDLLinkerException("Choice '$choice.name' cannot be extend itself!");
			choice.parents.add ( parent );
						
		}
		
		choice.referencesLinked = true;
		
		// Resolve all items for full inheritance path
		def allItems = [] as List;
		def inheritancePath = [:] as Map;
		getAllChoiceItems ( choice, allItems, inheritancePath, namespace );
		
		validateUniqueNames ( allItems, "for '$choice.name'" );
		
		choice.setAllItems (allItems);
	}
	
	def linkInterfaces ( namespace ) {
		namespace.interfaces.each {
			linkInterface ( it, namespace );
		}		
	}
	
	def linkInterface ( srcInterface, namespace ) {

		if ( srcInterface.referencesLinked ) {
			return;
		}
		
		srcInterface.operations.each {
			linkInterfaceOperation ( it, srcInterface, namespace );
		}
		
		
		srcInterface.parentsLinks.each {
			def parent = lookupRefName ( it, namespace, ["interfaces", "typedefs"] );
			if (!parent)
				throw new IDLLinkerException("Unable to find parent interface '$it' for '$srcInterface.name'!");
			if (parent.is(srcInterface))
				throw new IDLLinkerException("Interface '$srcInterface.name' cannot be extend itself!");
			srcInterface.parents.add ( parent );						
		}
		
		srcInterface.referencesLinked = true;
		
		// Resolve all operations for full inheritance path
		def allItems = [] as List;
		def inheritancePath = [:] as Map;
		getAllInterfaceOperations ( srcInterface, allItems, inheritancePath, namespace );		
		
		if(allItems.size()>1) {			
			// Validating unique constraint of operations			
			for (def i=0;i<allItems.size()-1;i++ ) {	
				for (def j=i+1;j<allItems.size();j++ ) {
					if (allItems[i].isEqualsToOperation(allItems[j])) {
						throw new IDLLinkerException("There is duplicate interface operation '${srcInterface.name}.${allItems[i].name}' detected!");
					}
					
					if (allItems[i].name.equals(allItems[j].name)) {
						allItems[i].signature = generateSignature(allItems[i]);
					}
				}
			}			
		}
		
		// Filling other unique operation signatures
		allItems.each {
			if(it.signature == null)
				it.signature = it.name;
		}

		
		srcInterface.setAllOperations (allItems);
	}
	
	def generateSignature (IDLInterfaceOperation operation ) {
		String signature = operation.name;
		operation.params.each {
			signature += ":"+generateTypeSignature(it.type);
		}
		return signature;
	}
	
	def generateTypeSignature ( IDLDataType dataType ) {
		if ( dataType.typeId == IDLDataType.DataTypeId.PRIMITIVE ) {
			return "P"+dataType.primitiveType.typeId.getTypeIdValue();
		}
		else
		if ( dataType.typeId == IDLDataType.DataTypeId.COMPLEX ) {
			if(dataType.complexType.complexTypeId == IDLComplexType.ComplexTypeId.ARRAY)
				return "A("+generateTypeSignature(dataType.complexType.complexItemDataType)+")";
			else
			if(dataType.complexType.complexTypeId == IDLComplexType.ComplexTypeId.LIST) {
				return "L("+generateTypeSignature(dataType.complexType.complexItemDataType);+")";
			}
			else
			if(dataType.complexType.complexTypeId == IDLComplexType.ComplexTypeId.MAP)
				return "M("+generateTypeSignature(dataType.complexType.complexKeyDataType)+"/"+generateTypeSignature(dataType.complexType.complexItemDataType)+")";				
		}
		else
		if ( dataType.typeId == IDLDataType.DataTypeId.QUALIFIED_NAME ) {
			return "Q("+dataType.userObject.name+")";
		}
	}
	
	def getAllInterfaceOperations ( srcInterface, allItems, inheritancePath, namespace ) {
		def pathItem = inheritancePath.get(srcInterface);
		
		if( pathItem == null ) {
			inheritancePath.put ( srcInterface, true );

			if ( srcInterface instanceof IDLTypeDef ) {
				def typeDef = srcInterface;
				if ( ! typeDef.referencesLinked ) {
					linkTypeDef ( typeDef, typeDef.namespace );
				}

				if ( !typeDef.refDataType.typeId.equals(IDLDataType.DataTypeId.QUALIFIED_NAME) )
					throw new IDLLinkerException("Primitive type: ${typeDef.name} cannot define interface operations!");
				if ( !typeDef.refDataType.userObject instanceof IDLTypeDef || !typeDef.refDataType.userObject instanceof IDLInterface )
					throw new IDLLinkerException("${typeDef.name} is not compatible data type for interfaces!");
				
				getAllInterfaceOperations ( typeDef.refDataType.userObject, allItems, inheritancePath, typeDef.namespace );
			}
			else {
				allItems.addAll ( srcInterface.operations );
				srcInterface.parents.each {
					if(!it.referencesLinked)
						linkInterface ( it, it.namespace );
					getAllInterfaceOperations ( it, allItems, inheritancePath, it.namespace );
				};
			}
			inheritancePath[srcInterface] = false;
		}
		else {
			if(pathItem.value)
				throw new IDLLinkerException("Found cyclic inheritance path for ${srcInterface.name}!");
		}		
	}
	
	def linkInterfaceOperation ( oper, srcInterface, namespace ) {
		// Resolving result datatype
		if(oper.resultType)
			resolveDataType ( oper.resultType, namespace );
		
		// Resolving params
		oper.params.each {			
			resolveDataType ( it.type, namespace );
		}
		
		// Checking unique constraints for param names
		validateUniqueNames ( oper.params, "for parameters of '$srcInterface.name.$oper.name'" );
		
		int sessionIdPrmCnt = 0;
		oper.params.each {
			if(it.isSessionId() && ++sessionIdPrmCnt>1)
				throw new IDLLinkerException("Unable to define more than one parameter as session id in '$srcInterface.name.$oper.name'!");
		}
		
		// Resolving throws declarations
		oper.throwsDeclLinks.each {
			def throwDecl = lookupRefName ( it, namespace, ["exceptions", "typedefs"], true );
			if (!throwDecl)
				throw new IDLLinkerException("Unable to found throws exception declaration '$it' for '$srcInterface.name.$oper.name'!");			
			oper.throwsDecl.add ( throwDecl );
		}
	}
}
